
#include "SDL.h"
#include "gl3.h"

namespace {

#define NL "\n"
const char *g_src_vs =
    NL "#version 130"
    NL "in vec4 in_vertex;"
    NL "void main() {"
    NL "    gl_Position = in_vertex;"
    NL "}";
const char *g_src_fs =
    NL "#version 130"
    NL "out vec4 fs_color;"
    NL "uniform vec2 u_resolution;"
    NL "uniform float u_time;"
    NL
    NL "vec3 compute_color(vec2 p) {"
    NL "    vec3 col = 0.5*(1.0+vec3(p.y));"
    NL "    return col;"
    NL "}"
    NL "void main() {"
    NL "    vec2 p = -1.0+2.0*gl_FragCoord.xy/u_resolution;"
    NL "    fs_color = vec4(compute_color(p), 1.0);"
    NL "}";
#undef NL

// shader
PFNGLCREATESHADERPROC glCreateShader;
PFNGLDELETESHADERPROC glDeleteShader;
PFNGLSHADERSOURCEPROC glShaderSource;
PFNGLCOMPILESHADERPROC glCompileShader;
// program
PFNGLCREATEPROGRAMPROC glCreateProgram;
PFNGLDELETEPROGRAMPROC glDeleteProgram;
PFNGLLINKPROGRAMPROC glLinkProgram;
PFNGLUSEPROGRAMPROC glUseProgram;
PFNGLATTACHSHADERPROC glAttachShader;
PFNGLGETPROGRAMIVPROC glGetProgramiv;
PFNGLGETPROGRAMINFOLOGPROC glGetProgramInfoLog;
PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation;
PFNGLUNIFORM1FPROC glUniform1f;
PFNGLUNIFORM2FPROC glUniform2f;
// vertex array
PFNGLGENVERTEXARRAYSPROC glGenVertexArrays;
PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArrays;
PFNGLBINDVERTEXARRAYPROC glBindVertexArray;
PFNGLVERTEXATTRIBPOINTERPROC glVertexAttribPointer;
PFNGLENABLEVERTEXATTRIBARRAYPROC glEnableVertexAttribArray;
// buffer
PFNGLGENBUFFERSPROC glGenBuffers;
PFNGLDELETEBUFFERSPROC glDeleteBuffers;
PFNGLBINDBUFFERPROC glBindBuffer;
PFNGLBUFFERDATAPROC glBufferData;
// draw call
PFNGLDRAWARRAYSPROC glDrawArrays;

SDL_WindowID g_window;
SDL_GLContext g_context;
GLuint g_program;
GLuint g_vao;
GLuint g_vbo;

void *load_opengl_command(const char *cmd) {
    void *r = SDL_GL_GetProcAddress(cmd);
    if (r == NULL) {
        SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Failed to load %s command.", cmd);
    }

    return r;
}

void load_opengl_commands() {
    // shader
    glCreateShader = reinterpret_cast<PFNGLCREATESHADERPROC>(load_opengl_command("glCreateShader"));
    glDeleteShader = reinterpret_cast<PFNGLDELETESHADERPROC>(load_opengl_command("glDeleteShader"));
    glShaderSource = reinterpret_cast<PFNGLSHADERSOURCEPROC>(load_opengl_command("glShaderSource"));
    glCompileShader = reinterpret_cast<PFNGLCOMPILESHADERPROC>(load_opengl_command("glCompileShader"));
    // program
    glCreateProgram = reinterpret_cast<PFNGLCREATEPROGRAMPROC>(load_opengl_command("glCreateProgram"));
    glDeleteProgram = reinterpret_cast<PFNGLDELETEPROGRAMPROC>(load_opengl_command("glDeleteProgram"));
    glLinkProgram = reinterpret_cast<PFNGLLINKPROGRAMPROC>(load_opengl_command("glLinkProgram"));
    glUseProgram = reinterpret_cast<PFNGLUSEPROGRAMPROC>(load_opengl_command("glUseProgram"));
    glAttachShader = reinterpret_cast<PFNGLATTACHSHADERPROC>(load_opengl_command("glAttachShader"));
    glGetProgramiv = reinterpret_cast<PFNGLGETPROGRAMIVPROC>(load_opengl_command("glGetProgramiv"));
    glGetProgramInfoLog = reinterpret_cast<PFNGLGETPROGRAMINFOLOGPROC>(load_opengl_command("glGetProgramInfoLog"));
    glGetUniformLocation = reinterpret_cast<PFNGLGETUNIFORMLOCATIONPROC>(load_opengl_command("glGetUniformLocation"));
    glUniform1f = reinterpret_cast<PFNGLUNIFORM1FPROC>(load_opengl_command("glUniform1f"));
    glUniform2f = reinterpret_cast<PFNGLUNIFORM2FPROC>(load_opengl_command("glUniform2f"));
    // vertex array
    glGenVertexArrays = reinterpret_cast<PFNGLGENVERTEXARRAYSPROC>(load_opengl_command("glGenVertexArrays"));
    glDeleteVertexArrays = reinterpret_cast<PFNGLDELETEVERTEXARRAYSPROC>(load_opengl_command("glDeleteVertexArrays"));
    glBindVertexArray = reinterpret_cast<PFNGLBINDVERTEXARRAYPROC>(load_opengl_command("glBindVertexArray"));
    glVertexAttribPointer = reinterpret_cast<PFNGLVERTEXATTRIBPOINTERPROC>(load_opengl_command("glVertexAttribPointer"));
    glEnableVertexAttribArray = reinterpret_cast<PFNGLENABLEVERTEXATTRIBARRAYPROC>(load_opengl_command("glEnableVertexAttribArray"));
    // buffer
    glGenBuffers = reinterpret_cast<PFNGLGENBUFFERSPROC>(load_opengl_command("glGenBuffers"));
    glDeleteBuffers = reinterpret_cast<PFNGLDELETEBUFFERSPROC>(load_opengl_command("glDeleteBuffers"));
    glBindBuffer = reinterpret_cast<PFNGLBINDBUFFERPROC>(load_opengl_command("glBindBuffer"));
    glBufferData = reinterpret_cast<PFNGLBUFFERDATAPROC>(load_opengl_command("glBufferData"));
    // draw call
    glDrawArrays = reinterpret_cast<PFNGLDRAWARRAYSPROC>(load_opengl_command("glDrawArrays"));
}

bool events() {
    SDL_Event evt;
    while (SDL_PollEvent(&evt)) {
        switch (evt.type) {
        case SDL_QUIT:
            return true; // quit
        case SDL_KEYDOWN:
            if (evt.key.keysym.sym == SDLK_ESCAPE) {
                return true; // quit
            }
            break;
        }
    }

    return false;
}

int frame() {
    const float time = SDL_GetTicks() * 0.001f;

    glUseProgram(g_program);
    glUniform1f(glGetUniformLocation(g_program, "u_time"), time);
    glBindVertexArray(g_vao);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

    return 0;
}

int init() {
    // SDL
    if (SDL_Init(SDL_INIT_TIMER | SDL_INIT_VIDEO) < 0) {
        SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, "SDL_Init failed.");
        return -1;
    }
    SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG);

    // window
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
    SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
    SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);

    const int w = 800, h = 600;
    g_window = SDL_CreateWindow("impro1", 10, 10, w, h, SDL_WINDOW_SHOWN | SDL_WINDOW_OPENGL | SDL_WINDOW_BORDERLESS);
    if (g_window == NULL) {
        SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateWindow failed.");
        return -1;
    }

    // context
    g_context = SDL_GL_CreateContext(g_window);
    if (g_context == NULL) {
        SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, "SDL_GL_CreateContext failed.");
        return -1;
    }
    load_opengl_commands();
    SDL_GL_SetSwapInterval(0);

    // vbo
    const float vertices[] = { -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f };
    glGenBuffers(1, &g_vbo);
    glBindBuffer(GL_ARRAY_BUFFER, g_vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    // vao
    glGenVertexArrays(1, &g_vao);
    glBindVertexArray(g_vao);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(0);

    // program
    g_program = glCreateProgram();

    GLuint sh = glCreateShader(GL_VERTEX_SHADER);
    glAttachShader(g_program, sh);
    glDeleteShader(sh);
    glShaderSource(sh, 1, &g_src_vs, NULL);
    glCompileShader(sh);

    sh = glCreateShader(GL_FRAGMENT_SHADER);
    glAttachShader(g_program, sh);
    glDeleteShader(sh);
    glShaderSource(sh, 1, &g_src_fs, NULL);
    glCompileShader(sh);

    GLint link_status;
    glLinkProgram(g_program);
    glGetProgramiv(g_program, GL_LINK_STATUS, &link_status);
    if (link_status == GL_FALSE) {
        GLchar log[2048];
        glGetProgramInfoLog(g_program, sizeof(log), NULL, log);
        SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, "GLSL link error. Info log:\n%s.", log);
        return -1;
    }

    glUseProgram(g_program);
    glUniform2f(glGetUniformLocation(g_program, "u_resolution"), w, h);

    return 0;
}

int run() {
    while (true) {
        if (events()) break;
        if (frame() < 0) return -1;
        SDL_GL_SwapWindow(g_window);
    }

    return 0;
}

int quit() {
    if (g_context) {
        glDeleteBuffers(1, &g_vbo);
        glDeleteVertexArrays(1, &g_vao);
        glDeleteProgram(g_program);

        SDL_GL_DeleteContext(g_context);
    }

    SDL_DestroyWindow(g_window);
    SDL_Quit();

    return 0;
}

} // namespace

int main(int argc, char *argv[]) {
    if (init() == 0) {
        run();
    }
    quit();
    return 0;
}
